package com.thghh.bcode.compiler;

import org.eclipse.jdt.core.compiler.CategorizedProblem;
import org.eclipse.jdt.core.compiler.CharOperation;
import org.eclipse.jdt.internal.compiler.*;
import org.eclipse.jdt.internal.compiler.Compiler;
import org.eclipse.jdt.internal.compiler.apt.dispatch.BaseAnnotationProcessorManager;
import org.eclipse.jdt.internal.compiler.apt.dispatch.BaseMessagerImpl;
import org.eclipse.jdt.internal.compiler.apt.dispatch.BaseProcessingEnvImpl;
import org.eclipse.jdt.internal.compiler.apt.dispatch.ProcessorInfo;
import org.eclipse.jdt.internal.compiler.classfmt.ClassFileReader;
import org.eclipse.jdt.internal.compiler.classfmt.ClassFormatException;
import org.eclipse.jdt.internal.compiler.env.IBinaryType;
import org.eclipse.jdt.internal.compiler.env.ICompilationUnit;
import org.eclipse.jdt.internal.compiler.env.INameEnvironment;
import org.eclipse.jdt.internal.compiler.env.NameEnvironmentAnswer;
import org.eclipse.jdt.internal.compiler.impl.CompilerOptions;
import org.eclipse.jdt.internal.compiler.lookup.BinaryTypeBinding;
import org.eclipse.jdt.internal.compiler.lookup.ReferenceBinding;
import org.eclipse.jdt.internal.compiler.problem.AbortCompilation;
import org.eclipse.jdt.internal.compiler.problem.DefaultProblemFactory;
import org.eclipse.jdt.internal.compiler.tool.EclipseFileManager;
import org.eclipse.jdt.internal.compiler.util.SuffixConstants;

import javax.annotation.processing.Filer;
import javax.annotation.processing.FilerException;
import javax.annotation.processing.Messager;
import javax.annotation.processing.Processor;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.AnnotationValue;
import javax.lang.model.element.Element;
import javax.tools.*;
import javax.tools.JavaFileManager.Location;
import java.io.*;
import java.net.URI;
import java.nio.charset.Charset;
import java.util.*;

import static org.eclipse.jdt.internal.compiler.impl.CompilerOptions.*;

/**
 * @author Zhikang.Peng
 * @version 1.0
 * @email thghh@qq.com
 * @date 2020/12/17 16:38
 */
public class EclipseJavaCompiler implements DynamicJavaCompiler {

    private ByteCodeFileManager fileManager;
    private CompilerOptions compilerOptions;
    private CompilerRequestor requestor;
    private Map<String, String> settings = new HashMap<>();
    ;

    public EclipseJavaCompiler() {
        fileManager = new ByteCodeFileManager(new EclipseFileManager(Locale.getDefault(), Charset.defaultCharset()));
        compilerOptions = getDefaultCompilerOptions();
        requestor = new CompilerRequestor();
    }

    private CompilerOptions getDefaultCompilerOptions() {
        settings.put(OPTION_ReportMissingSerialVersion, IGNORE);
        settings.put(OPTION_LineNumberAttribute, GENERATE);
        settings.put(OPTION_SourceFileAttribute, GENERATE);
        settings.put(OPTION_ReportDeprecation, IGNORE);
        settings.put(OPTION_ReportUnusedImport, IGNORE);
        settings.put(OPTION_Encoding, "UTF-8");
        settings.put(OPTION_LocalVariableAttribute, GENERATE);

        String javaVersion = VERSION_1_6;
        try {
            String version = System.getProperty("java.version");
            if (version.startsWith("1.5"))
                javaVersion = VERSION_1_5;
            else if (version.startsWith("1.6"))
                javaVersion = VERSION_1_6;
            else if (version.startsWith("1.7"))
                javaVersion = VERSION_1_7;
            else if (version.startsWith("1.8"))
                javaVersion = VERSION_1_8;
            else if (version.startsWith("1.9"))
                javaVersion = VERSION_1_9;
        } catch (Exception e) {
        }

        settings.put(OPTION_Source, javaVersion);
        settings.put(OPTION_TargetPlatform, javaVersion);
        settings.put(OPTION_PreserveUnusedLocal, PRESERVE);
        settings.put(OPTION_Compliance, javaVersion);
        settings.put(OPTION_Process_Annotations, ENABLED);
        settings.put(OPTION_Store_Annotations, ENABLED);
        settings.put(OPTION_AnnotationBasedNullAnalysis,  ENABLED);
        settings.put(OPTION_ReportNullAnnotationInferenceConflict, "INFO");
        settings.put(OPTION_ReportNullUncheckedConversion, "INFO");
        settings.put(OPTION_ReportRedundantNullAnnotation, "INFO");
        settings.put(OPTION_InheritNullAnnotations, ENABLED);
        return new CompilerOptions(settings);
    }

    @Override
    public boolean javaCompiler(String className, String sourceCode) {
        CompilationUnit compilationUnit = new CompilationUnit(className, sourceCode);
        CompilerEnv env = new CompilerEnv(className, compilationUnit);
        IErrorHandlingPolicy policy = DefaultErrorHandlingPolicies.exitAfterAllProblems();
        IProblemFactory problemFactory = new DefaultProblemFactory(Locale.getDefault());

        try {
            //Compiler compiler = new Compiler(env, policy, getCompilerOptions(), requestor, problemFactory, true);
            Compiler compiler = new Compiler(env, policy, compilerOptions, requestor, problemFactory);
            compiler.annotationProcessorManager = new MyAnnotationProcessorManager();
            compiler.annotationProcessorManager.configure(compiler, new String[]{"-XprintProcessorInfo", "-XprintRounds"});
            compiler.annotationProcessorManager.setErr(new PrintWriter(System.err));
            compiler.annotationProcessorManager.setOut(new PrintWriter(System.out));
            compiler.compile(new ICompilationUnit[]{compilationUnit});

            List<CategorizedProblem> problemList = requestor.getProblemList();
            if (!problemList.isEmpty()) {
                StringBuilder sb = new StringBuilder();
                for (CategorizedProblem problem : problemList) {
                    sb.append("\t").append(problem.toString()).append("\n");
                }
                throw new CompilerException("Compilation message: \n" + sb.toString());
            }
        } catch (Exception ex) {
            throw new CompilerException(ex);
        }
        return true;
    }

    @Override
    public Class loadClass(String className) throws ClassNotFoundException {
        return fileManager.getClassLoader(StandardLocation.CLASS_OUTPUT).loadClass(className);
    }

    @Override
    public byte[] getByteCode() {
        return requestor.getByteCodeJavaFileObject().getBytes();
    }

    @Override
    public void close() throws IOException {
        fileManager.close();
    }

    /**
     * Java 6 annotation processor manager used when compiling from the command line
     * or via the javax.tools.JavaCompiler interface.
     */
    public class MyAnnotationProcessorManager extends BaseAnnotationProcessorManager {

        /**
         * Processors that have been set by calling CompilationTask.setProcessors().
         */
        private List<Processor> _setProcessors = null;
        private Iterator<Processor> _setProcessorIter = null;

        /**
         * Processors named with the -processor option on the command line.
         */
        private List<String> _commandLineProcessors;
        private Iterator<String> _commandLineProcessorIter = null;

        private ServiceLoader<Processor> _serviceLoader = null;
        private Iterator<Processor> _serviceLoaderIter;

        private ClassLoader _procLoader;

        // Set this to true in order to trace processor discovery when -XprintProcessorInfo is specified
        private final static boolean VERBOSE_PROCESSOR_DISCOVERY = true;
        private boolean _printProcessorDiscovery = false;

        /**
         * Zero-arg constructor so this object can be easily created via reflection.
         * A MyAnnotationProcessorManager cannot be used until its
         * {@link #configure(Object, String[])} method has been called.
         */
        public MyAnnotationProcessorManager() {
        }

        @Override
        public void configure(Object batchCompiler, String[] commandLineArguments) {
            if (null != _processingEnv) {
                throw new IllegalStateException(
                        "Calling configure() more than once on an AnnotationProcessorManager is not supported"); //$NON-NLS-1$
            }
            MyBatchProcessingEnvImpl processingEnv = new MyBatchProcessingEnvImpl(this, (Compiler) batchCompiler, commandLineArguments);
            _processingEnv = processingEnv;
            _procLoader = processingEnv.getFileManager().getClassLoader(StandardLocation.ANNOTATION_PROCESSOR_PATH);
            parseCommandLine(commandLineArguments);
            _round = 0;
        }

        /**
         * If a -processor option was specified in command line arguments,
         * parse it into a list of qualified classnames.
         *
         * @param commandLineArguments contains one string for every space-delimited token on the command line
         */
        private void parseCommandLine(String[] commandLineArguments) {
            List<String> commandLineProcessors = null;
            for (int i = 0; i < commandLineArguments.length; ++i) {
                String option = commandLineArguments[i];
                if ("-XprintProcessorInfo".equals(option)) { //$NON-NLS-1$
                    _printProcessorInfo = true;
                    _printProcessorDiscovery = VERBOSE_PROCESSOR_DISCOVERY;
                } else if ("-XprintRounds".equals(option)) { //$NON-NLS-1$
                    _printRounds = true;
                } else if ("-processor".equals(option)) { //$NON-NLS-1$
                    commandLineProcessors = new ArrayList<>();
                    String procs = commandLineArguments[++i];
                    for (String proc : procs.split(",")) { //$NON-NLS-1$
                        commandLineProcessors.add(proc);
                    }
                    break;
                }
            }
            _commandLineProcessors = commandLineProcessors;
            if (null != _commandLineProcessors) {
                _commandLineProcessorIter = _commandLineProcessors.iterator();
            }
        }

        @Override
        public ProcessorInfo discoverNextProcessor() {
            if (null != _setProcessors) {
                // If setProcessors() was called, use that list until it's empty and then stop.
                if (_setProcessorIter.hasNext()) {
                    Processor p = _setProcessorIter.next();
                    p.init(_processingEnv);
                    ProcessorInfo pi = new ProcessorInfo(p);
                    _processors.add(pi);
                    if (_printProcessorDiscovery && null != _out) {
                        _out.println("API specified processor: " + pi); //$NON-NLS-1$
                    }
                    return pi;
                }
                return null;
            }

            if (null != _commandLineProcessors) {
                // If there was a -processor option, iterate over processor names,
                // creating and initializing processors, until no more names are found, then stop.
                if (_commandLineProcessorIter.hasNext()) {
                    String proc = _commandLineProcessorIter.next();
                    try {
                        Class<?> clazz = _procLoader.loadClass(proc);
                        Object o = clazz.newInstance();
                        Processor p = (Processor) o;
                        p.init(_processingEnv);
                        ProcessorInfo pi = new ProcessorInfo(p);
                        _processors.add(pi);
                        if (_printProcessorDiscovery && null != _out) {
                            _out.println("Command line specified processor: " + pi); //$NON-NLS-1$
                        }
                        return pi;
                    } catch (Exception e) {
                        // TODO: better error handling
                        throw new AbortCompilation(null, e);
                    }
                }
                return null;
            }

            // if no processors were explicitly specified with setProcessors()
            // or the command line, search the processor path with ServiceLoader.
            if (null == _serviceLoader) {
                _serviceLoader = ServiceLoader.load(Processor.class, _procLoader);
                _serviceLoaderIter = _serviceLoader.iterator();
            }
            try {
                if (_serviceLoaderIter.hasNext()) {
                    Processor p = _serviceLoaderIter.next();
                    p.init(_processingEnv);
                    ProcessorInfo pi = new ProcessorInfo(p);
                    _processors.add(pi);
                    if (_printProcessorDiscovery && null != _out) {
                        StringBuilder sb = new StringBuilder();
                        sb.append("Discovered processor service "); //$NON-NLS-1$
                        sb.append(pi);
                        sb.append("\n  supporting "); //$NON-NLS-1$
                        sb.append(pi.getSupportedAnnotationTypesAsString());
                        sb.append("\n  in "); //$NON-NLS-1$
                        sb.append(getProcessorLocation(p));
                        _out.println(sb.toString());
                    }
                    return pi;
                }
            } catch (ServiceConfigurationError e) {
                // TODO: better error handling
                throw new AbortCompilation(null, e);
            }
            return null;
        }

        /**
         * Used only for debugging purposes.  Generates output like "file:jar:D:/temp/jarfiles/myJar.jar!/".
         * Surely this code already exists in several hundred other places?
         *
         * @return the location whence a processor class was loaded.
         */
        private String getProcessorLocation(Processor p) {
            // Get the classname in a form that can be passed to ClassLoader.getResource(),
            // e.g., "pa/pb/pc/Outer$Inner.class"
            boolean isMember = false;
            Class<?> outerClass = p.getClass();
            StringBuilder innerName = new StringBuilder();
            while (outerClass.isMemberClass()) {
                innerName.insert(0, outerClass.getSimpleName());
                innerName.insert(0, '$');
                isMember = true;
                outerClass = outerClass.getEnclosingClass();
            }
            String path = outerClass.getName();
            path = path.replace('.', '/');
            if (isMember) {
                path = path + innerName;
            }
            path = path + ".class"; //$NON-NLS-1$

            // Find the URL for the class resource and strip off the resource name itself
            String location = _procLoader.getResource(path).toString();
            if (location.endsWith(path)) {
                location = location.substring(0, location.length() - path.length());
            }
            return location;
        }

        @Override
        public void reportProcessorException(Processor p, Exception e) {
            // TODO: if (verbose) report the processor
            throw new AbortCompilation(null, e);
        }

        @Override
        public void setProcessors(Object[] processors) {
            if (!_isFirstRound) {
                throw new IllegalStateException("setProcessors() cannot be called after processing has begun"); //$NON-NLS-1$
            }
            // Cast all the processors here, rather than failing later.
            // But don't call init() until the processor is actually needed.
            _setProcessors = new ArrayList<>(processors.length);
            for (Object o : processors) {
                Processor p = (Processor) o;
                _setProcessors.add(p);
            }
            _setProcessorIter = _setProcessors.iterator();

            // processors set this way take precedence over anything on the command line
            _commandLineProcessors = null;
            _commandLineProcessorIter = null;
        }

    }


    /**
     * The implementation of ProcessingEnvironment that is used when compilation is
     * driven by the command line or by the Tool interface.  This environment uses
     * the JavaFileManager provided by the compiler.
     */
    public class MyBatchProcessingEnvImpl extends BaseProcessingEnvImpl {

        protected final BaseAnnotationProcessorManager _dispatchManager;
        protected final JavaFileManager _fileManager;

        public MyBatchProcessingEnvImpl(BaseAnnotationProcessorManager dispatchManager, Compiler batchCompiler,
                                        String[] commandLineArguments) {
            super();
            _compiler = batchCompiler;
            _dispatchManager = dispatchManager;

            String encoding = settings.get(CompilerOptions.OPTION_Encoding);
            Charset charset = encoding != null ? Charset.forName(encoding) : null;
            JavaFileManager manager = new EclipseFileManager(Locale.getDefault(), charset);
            ArrayList<String> options = new ArrayList<>();
            for (String argument : commandLineArguments) {
                options.add(argument);
            }
            for (Iterator<String> iterator = options.iterator(); iterator.hasNext(); ) {
                manager.handleOption(iterator.next(), iterator);
            }
            _fileManager = manager;

            _processorOptions = Collections.unmodifiableMap(parseProcessorOptions(commandLineArguments));
            _filer = new MyBatchFilerImpl(_dispatchManager, this);
            _messager = new MyBatchMessagerImpl(this);
        }

        /**
         * Parse the -A command line arguments so that they can be delivered to
         * processors with {@link javax.annotation.processing.ProcessingEnvironment#getOptions()}.  In Sun's Java 6
         * version of javac, unlike in the Java 5 apt tool, only the -A options are
         * passed to processors, not the other command line options; that behavior
         * is repeated here.
         *
         * @param args the equivalent of the args array from the main() method.
         * @return a map of key to value, or key to null if there is no value for
         * a particular key.  The "-A" is stripped from the key, so a command-line
         * argument like "-Afoo=bar" will result in an entry with key "foo" and
         * value "bar".
         */
        private Map<String, String> parseProcessorOptions(String[] args) {
            Map<String, String> options = new LinkedHashMap<>();
            for (String arg : args) {
                if (!arg.startsWith("-A")) { //$NON-NLS-1$
                    continue;
                }
                int equals = arg.indexOf('=');
                if (equals == 2) {
                    // option begins "-A=" - not valid
                    Exception e = new IllegalArgumentException("-A option must have a key before the equals sign"); //$NON-NLS-1$
                    throw new AbortCompilation(null, e);
                }
                if (equals == arg.length() - 1) {
                    // option ends with "=" - not valid
                    options.put(arg.substring(2, equals), null);
                } else if (equals == -1) {
                    // no value
                    options.put(arg.substring(2), null);
                } else {
                    // value and key
                    options.put(arg.substring(2, equals), arg.substring(equals + 1));
                }
            }
            return options;
        }

        public JavaFileManager getFileManager() {
            return _fileManager;
        }

        @Override
        public Locale getLocale() {
            return Locale.getDefault();
        }

    }

    /**
     * Implementation of Filer used when compilation is driven by command line
     * or by Tool interface.  This version does not need to keep track of
     * dependencies.
     */
    public class MyBatchFilerImpl implements Filer {

        protected final BaseAnnotationProcessorManager _dispatchManager;
        protected final MyBatchProcessingEnvImpl _env;
        protected final JavaFileManager _fileManager;
        protected final HashSet<URI> _createdFiles;

        public MyBatchFilerImpl(BaseAnnotationProcessorManager dispatchManager, MyBatchProcessingEnvImpl env) {
            _dispatchManager = dispatchManager;
            _fileManager = env._fileManager;
            _env = env;
            _createdFiles = new HashSet<>();
        }

        public void addNewUnit(ICompilationUnit unit) {
            _env.addNewUnit(unit);
        }

        public void addNewClassFile(ReferenceBinding binding) {
            _env.addNewClassFile(binding);
        }

        /* (non-Javadoc)
         * @see javax.annotation.processing.Filer#createClassFile(java.lang.CharSequence, javax.lang.model.element.Element[])
         */
        @Override
        public JavaFileObject createClassFile(CharSequence name,
                                              Element... originatingElements) throws IOException {
            JavaFileObject jfo = _fileManager.getJavaFileForOutput(
                    StandardLocation.CLASS_OUTPUT, name.toString(), JavaFileObject.Kind.CLASS, null);
            URI uri = jfo.toUri();
            if (_createdFiles.contains(uri)) {
                throw new FilerException("Class file already created : " + name); //$NON-NLS-1$
            }

            _createdFiles.add(uri);
            return new MyHookedJavaFileObject(jfo, jfo.getName(), name.toString(), this);
        }

        /* (non-Javadoc)
         * @see javax.annotation.processing.Filer#createResource(javax.tools.JavaFileManager.Location, java.lang.CharSequence, java.lang.CharSequence, javax.lang.model.element.Element[])
         */
        @Override
        public FileObject createResource(Location location, CharSequence pkg,
                                         CharSequence relativeName, Element... originatingElements)
                throws IOException {
            validateName(relativeName);
            FileObject fo = _fileManager.getFileForOutput(
                    location, pkg.toString(), relativeName.toString(), null);
            URI uri = fo.toUri();
            if (_createdFiles.contains(uri)) {
                throw new FilerException("Resource already created : " + location + '/' + pkg + '/' + relativeName); //$NON-NLS-1$
            }

            _createdFiles.add(uri);
            return fo;
        }

        private void validateName(CharSequence relativeName) {
            int length = relativeName.length();
            if (length == 0) {
                throw new IllegalArgumentException("relative path cannot be empty"); //$NON-NLS-1$
            }
            String path = relativeName.toString();
            if (path.indexOf('\\') != -1) {
                // normalize the path with '/'
                path = path.replace('\\', '/');
            }
            if (path.charAt(0) == '/') {
                throw new IllegalArgumentException("relative path is absolute"); //$NON-NLS-1$
            }
            boolean hasDot = false;
            for (int i = 0; i < length; i++) {
                switch (path.charAt(i)) {
                    case '/':
                        if (hasDot) {
                            throw new IllegalArgumentException("relative name " + relativeName + " is not relative"); //$NON-NLS-1$ //$NON-NLS-2$
                        }
                        break;
                    case '.':
                        hasDot = true;
                        break;
                    default:
                        hasDot = false;
                }
            }
            if (hasDot) {
                throw new IllegalArgumentException("relative name " + relativeName + " is not relative"); //$NON-NLS-1$ //$NON-NLS-2$
            }
        }

        /* (non-Javadoc)
         * @see javax.annotation.processing.Filer#createSourceFile(java.lang.CharSequence, javax.lang.model.element.Element[])
         */
        @Override
        public JavaFileObject createSourceFile(CharSequence name,
                                               Element... originatingElements) throws IOException {
            JavaFileObject jfo = _fileManager.getJavaFileForOutput(
                    StandardLocation.SOURCE_OUTPUT, name.toString(), JavaFileObject.Kind.SOURCE, null);
            URI uri = jfo.toUri();
            if (_createdFiles.contains(uri)) {
                throw new FilerException("Source file already created : " + name); //$NON-NLS-1$
            }

            _createdFiles.add(uri);
            // hook the file object's writers to create compilation unit and add to addedUnits()
            return new MyHookedJavaFileObject(jfo, jfo.getName(), name.toString(), this);
        }

        /* (non-Javadoc)
         * @see javax.annotation.processing.Filer#getResource(javax.tools.JavaFileManager.Location, java.lang.CharSequence, java.lang.CharSequence)
         */
        @Override
        public FileObject getResource(Location location, CharSequence pkg,
                                      CharSequence relativeName) throws IOException {
            validateName(relativeName);
            FileObject fo = _fileManager.getFileForInput(
                    location, pkg.toString(), relativeName.toString());
            if (fo == null) {
                throw new FileNotFoundException("Resource does not exist : " + location + '/' + pkg + '/' + relativeName); //$NON-NLS-1$
            }
            URI uri = fo.toUri();
            if (_createdFiles.contains(uri)) {
                throw new FilerException("Resource already created : " + location + '/' + pkg + '/' + relativeName); //$NON-NLS-1$
            }

            _createdFiles.add(uri);
            return fo;
        }

    }


    /**
     * An implementation of Messager that reports messages via the Compiler
     */
    public class MyBatchMessagerImpl extends BaseMessagerImpl implements Messager {

        private final BaseProcessingEnvImpl _processingEnv;

        public MyBatchMessagerImpl(BaseProcessingEnvImpl processingEnv) {
            _processingEnv = processingEnv;
        }

        /* (non-Javadoc)
         * @see javax.annotation.processing.Messager#printMessage(javax.tools.Diagnostic.Kind, java.lang.CharSequence)
         */
        @Override
        public void printMessage(Diagnostic.Kind kind, CharSequence msg) {
            printMessage(kind, msg, null, null, null);
        }

        /* (non-Javadoc)
         * @see javax.annotation.processing.Messager#printMessage(javax.tools.Diagnostic.Kind, java.lang.CharSequence, javax.lang.model.element.Element)
         */
        @Override
        public void printMessage(Diagnostic.Kind kind, CharSequence msg, Element e) {
            printMessage(kind, msg, e, null, null);
        }

        /* (non-Javadoc)
         * @see javax.annotation.processing.Messager#printMessage(javax.tools.Diagnostic.Kind, java.lang.CharSequence, javax.lang.model.element.Element, javax.lang.model.element.AnnotationMirror)
         */
        @Override
        public void printMessage(Diagnostic.Kind kind, CharSequence msg, Element e,
                                 AnnotationMirror a) {
            printMessage(kind, msg, e, a, null);

        }

        /* (non-Javadoc)
         * @see javax.annotation.processing.Messager#printMessage(javax.tools.Diagnostic.Kind, java.lang.CharSequence, javax.lang.model.element.Element, javax.lang.model.element.AnnotationMirror, javax.lang.model.element.AnnotationValue)
         */
        @Override
        public void printMessage(Diagnostic.Kind kind, CharSequence msg, Element e,
                                 AnnotationMirror a, AnnotationValue v) {
            if (kind == Diagnostic.Kind.ERROR) {
                _processingEnv.setErrorRaised(true);
            }
            CategorizedProblem problem = createProblem(kind, msg, e, a, v);
            if (problem != null) {
                requestor.addExtraProblems(problem);
            }
        }
    }


    private class CompilerEnv implements INameEnvironment, SuffixConstants {

        private String className;
        private ICompilationUnit compilationUnit;
        private ClassLoader classLoader;
        private Map<String, Boolean> packagesCache;

        public CompilerEnv(String className, ICompilationUnit compilationUnit) {
            this.className = className;
            this.compilationUnit = compilationUnit;
            this.classLoader = DynamicJavaCompiler.class.getClassLoader();
            this.packagesCache = new HashMap<>();
        }

        @Override
        public NameEnvironmentAnswer findType(char[][] compoundTypeName) {
            System.out.println("findType(char[][] compoundTypeName) " + join(compoundTypeName, '.'));
            return findType(join(compoundTypeName, '.'));
        }

        @Override
        public NameEnvironmentAnswer findType(char[] typeName, char[][] packageName) {
            return findType(CharOperation.arrayConcat(packageName, typeName));
        }

        @Override
        public boolean isPackage(char[][] parentPackageName, char[] packageName) {
            // if the parent is a class, the child can't be a package
            String parent = join(parentPackageName, '.');
            // if the child is a class, it's not a package
            String qualifiedName = (parent.isEmpty() ? "" : parent + ".") + new String(packageName);
            System.out.println("isPackage(char[][] parentPackageName, char[] packageName) " + qualifiedName);
            if (packagesCache.containsKey(qualifiedName)) {
                return packagesCache.get(qualifiedName).booleanValue();
            }
            // if it's the class we're compiling, then of course it's a class
            if (qualifiedName.equals(className)) {
                packagesCache.put(qualifiedName, false);
                return false;
            }
            byte[] bytecode = getClassDefinition(qualifiedName);
            if (bytecode != null) {
                packagesCache.put(qualifiedName, false);
                return false;
            }
            return true;
        }

        @Override
        public void cleanup() {

        }

        private String join(char[][] compoundName, char separator) {
            if (compoundName == null) {
                return "";
            } else {
                StringBuilder appendable = new StringBuilder();
                appendable.append(compoundName[0]);
                for (int i = 1; i < compoundName.length; i++) {
                    appendable.append(separator);
                    appendable.append(compoundName[i]);
                }
                return appendable.toString();
            }
        }

        private byte[] getClassDefinition(String className) {
            String resourceName = className.replace('.', '/') + ".class";
            try (InputStream is = classLoader.getResourceAsStream(resourceName)) {
                if (is != null) {
                    return toByteArray(is);
                } else {
                    return null;
                }
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }

        private NameEnvironmentAnswer findType(String className) {
            // we're only asking ECJ to compile a single class; we shouldn't need this
            if (this.className.equals(className)) {
                return new NameEnvironmentAnswer(compilationUnit, null);
            } else {
                try {
                    byte[] bytecode = getClassDefinition(className);
                    if (bytecode != null) {
                        ClassFileReader cfr = new ClassFileReader(bytecode, className.toCharArray(), true);
                        return new NameEnvironmentAnswer(cfr, null);
                    } else {
                        return null;
                    }
                } catch (ClassFormatException ex) {
                    throw new RuntimeException(ex);
                }
            }
        }
    }

    private class CompilerRequestor implements ICompilerRequestor {

        private ByteCodeJavaFileObject jfo;
        private List<CategorizedProblem> problemList;

        public CompilerRequestor() {
            problemList = new ArrayList<>();
        }

        @Override
        public void acceptResult(CompilationResult result) {
            if (result.hasErrors()) {
                for (CategorizedProblem problem : result.getProblems()) {
                    if (problem.isError()) {
                        problemList.add(problem);
                    }
                }
            } else {
                for (ClassFile clazz : result.getClassFiles()) {
                    try {
                        jfo = (ByteCodeJavaFileObject) fileManager
                                .getJavaFileForOutput(StandardLocation.CLASS_OUTPUT,
                                        new String(clazz.fileName()), JavaFileObject.Kind.CLASS, null);
                        OutputStream os = jfo.openOutputStream();
                        os.write(clazz.getBytes());
                    } catch (IOException ex) {
                        throw new RuntimeException(ex);
                    }
                }
            }
        }

        public ByteCodeJavaFileObject getByteCodeJavaFileObject() {
            return jfo;
        }

        public List<CategorizedProblem> getProblemList() {
            return problemList;
        }

        public void addExtraProblems(CategorizedProblem problem) {
            problemList.add(problem);
        }
    }


    private class CompilationUnit implements ICompilationUnit {
        private String sourceCode;
        private String fileName;
        private char[] typeName;
        private char[][] packageName;

        public CompilationUnit(String className, String sourceCode) {
            this.sourceCode = sourceCode;
            fileName = className.replaceAll("\\.", "/") + JavaFileObject.Kind.CLASS.extension;
            int dot = className.lastIndexOf('.');
            if (dot > 0) {
                typeName = className.substring(dot + 1).toCharArray();
            } else {
                typeName = className.toCharArray();
            }
            StringTokenizer izer = new StringTokenizer(className, ".");
            packageName = new char[izer.countTokens() - 1][];
            for (int i = 0; i < packageName.length; i++) {
                packageName[i] = izer.nextToken().toCharArray();
            }
        }

        @Override
        public char[] getContents() {
            return sourceCode.toCharArray();
        }

        @Override
        public char[] getMainTypeName() {
            return typeName;
        }

        @Override
        public char[][] getPackageName() {
            return packageName;
        }

        @Override
        public boolean ignoreOptionalProblems() {
            return false;
        }

        @Override
        public char[] getFileName() {
            return fileName.toCharArray();
        }
    }

    /**
     * A delegating JavaFileObject that hooks the close() methods of the Writer
     * or OutputStream objects that it produces, and notifies the annotation
     * dispatch manager when a new compilation unit is produced.
     */
    public class MyHookedJavaFileObject extends
            ForwardingJavaFileObject<JavaFileObject> {
        // A delegating Writer that passes all commands to its contained Writer,
        // but hooks close() to notify the annotation dispatch manager of the new unit.
        private class ForwardingWriter extends Writer {
            private final Writer _w;

            ForwardingWriter(Writer w) {
                _w = w;
            }

            @Override
            public Writer append(char c) throws IOException {
                return _w.append(c);
            }

            @Override
            public Writer append(CharSequence csq, int start, int end)
                    throws IOException {
                return _w.append(csq, start, end);
            }

            @Override
            public Writer append(CharSequence csq) throws IOException {
                return _w.append(csq);
            }

            // This is the only interesting method - it has to notify the
            // dispatch manager of the new file.
            @Override
            public void close() throws IOException {
                _w.close();
                closed();
            }

            @Override
            public void flush() throws IOException {
                _w.flush();
            }

            @Override
            public void write(char[] cbuf) throws IOException {
                _w.write(cbuf);
            }

            @Override
            public void write(int c) throws IOException {
                _w.write(c);
            }

            @Override
            public void write(String str, int off, int len)
                    throws IOException {
                _w.write(str, off, len);
            }

            @Override
            public void write(String str) throws IOException {
                _w.write(str);
            }

            @Override
            public void write(char[] cbuf, int off, int len)
                    throws IOException {
                _w.write(cbuf, off, len);
            }

            @Override
            protected Object clone() throws CloneNotSupportedException {
                return new ForwardingWriter(this._w);
            }

            @Override
            public int hashCode() {
                return _w.hashCode();
            }

            @Override
            public boolean equals(Object obj) {
                if (this == obj)
                    return true;
                if (obj == null)
                    return false;
                if (getClass() != obj.getClass())
                    return false;
                final ForwardingWriter other = (ForwardingWriter) obj;
                if (_w == null) {
                    if (other._w != null)
                        return false;
                } else if (!_w.equals(other._w))
                    return false;
                return true;
            }

            @Override
            public String toString() {
                return "ForwardingWriter wrapping " + _w.toString(); //$NON-NLS-1$
            }
        }

        // A delegating Writer that passes all commands to its contained Writer,
        // but hooks close() to notify the annotation dispatch manager of the new unit.
        private class ForwardingOutputStream extends OutputStream {
            private final OutputStream _os;

            ForwardingOutputStream(OutputStream os) {
                _os = os;
            }

            @Override
            public void close() throws IOException {
                _os.close();
                closed();
            }

            @Override
            public void flush() throws IOException {
                _os.flush();
            }

            @Override
            public void write(byte[] b, int off, int len) throws IOException {
                _os.write(b, off, len);
            }

            @Override
            public void write(byte[] b) throws IOException {
                _os.write(b);
            }

            @Override
            public void write(int b) throws IOException {
                _os.write(b);
            }

            @Override
            protected Object clone() throws CloneNotSupportedException {
                return new ForwardingOutputStream(this._os);
            }

            @Override
            public int hashCode() {
                return _os.hashCode();
            }

            @Override
            public boolean equals(Object obj) {
                if (this == obj)
                    return true;
                if (obj == null)
                    return false;
                if (getClass() != obj.getClass())
                    return false;
                final ForwardingOutputStream other = (ForwardingOutputStream) obj;
                if (_os == null) {
                    if (other._os != null)
                        return false;
                } else if (!_os.equals(other._os))
                    return false;
                return true;
            }

            @Override
            public String toString() {
                return "ForwardingOutputStream wrapping " + _os.toString(); //$NON-NLS-1$
            }
        }

        /**
         * The Filer implementation that we need to notify when a new file is created.
         */
        protected final MyBatchFilerImpl _filer;

        /**
         * The name of the file that is created; this is passed to the CompilationUnit constructor,
         * and ultimately to the java.io.File constructor, so it is a normal pathname, just like
         * what would be on the compiler command line.
         */
        protected final String _fileName;


        /**
         * A compilation unit is created when the writer or stream is closed.  Only do this once.
         */
        private boolean _closed = false;

        private String _typeName;

        public MyHookedJavaFileObject(JavaFileObject fileObject, String fileName, String typeName, MyBatchFilerImpl filer) {
            super(fileObject);
            _filer = filer;
            _fileName = fileName;
            _typeName = typeName;
        }

        @Override
        public OutputStream openOutputStream() throws IOException {
            return new ForwardingOutputStream(super.openOutputStream());
        }

        @Override
        public Writer openWriter() throws IOException {
            return new ForwardingWriter(super.openWriter());
        }

        protected void closed() {
            if (!_closed) {
                _closed = true;
                //TODO: support encoding
                switch (this.getKind()) {
                    case SOURCE:
                        org.eclipse.jdt.internal.compiler.batch.CompilationUnit unit = new org.eclipse.jdt.internal.compiler.batch.CompilationUnit(null, _fileName, null /* encoding */);
                        _filer.addNewUnit(unit);
                        break;
                    case CLASS:
                        IBinaryType binaryType = null;
                        try {
                            binaryType = ClassFileReader.read(_fileName);
                        } catch (ClassFormatException e) {
						/* When the annotation processor produces garbage, javac seems to show some resilience, by hooking the source type,
						   which since is resolved can answer annotations during discovery - Not sure if this sanctioned by the spec, to be taken
						   up with Oracle. Here we mimic the bug, see that addNewClassFile is simply collecting ReferenceBinding's, so adding
						   a SourceTypeBinding works just fine.
						*/
                            ReferenceBinding type = this._filer._env.getCompiler().lookupEnvironment.getType(CharOperation.splitOn('.', _typeName.toCharArray()));
                            if (type != null)
                                _filer.addNewClassFile(type);
                        } catch (IOException e) {
                            // ignore
                        }
                        if (binaryType != null) {
                            char[] name = binaryType.getName();
                            ReferenceBinding type = this._filer._env.getCompiler().lookupEnvironment.getType(CharOperation.splitOn('/', name));
                            if (type != null && type.isValidBinding()) {
                                if (type.isBinaryBinding()) {
                                    _filer.addNewClassFile(type);
                                } else {
                                    BinaryTypeBinding binaryBinding = new BinaryTypeBinding(type.getPackage(), binaryType, this._filer._env.getCompiler().lookupEnvironment, true);
                                    if (binaryBinding != null)
                                        _filer.addNewClassFile(binaryBinding);
                                }
                            }
                        }
                        break;
                    case HTML:
                    case OTHER:
                        break;
                }
            }
        }
    }


    /**
     * Copies all bytes from the input stream to the output stream.
     * Does not close or flush either stream.
     *
     * @param from the input stream to read from
     * @param to   the output stream to write to
     * @return the number of bytes copied
     * @throws IOException if an I/O error occurs
     */
    private long copy(InputStream from, OutputStream to)
            throws IOException {
        Objects.requireNonNull(from);
        Objects.requireNonNull(to);
        byte[] buf = new byte[4096];
        long total = 0;
        while (true) {
            int r = from.read(buf);
            if (r == -1) {
                break;
            }
            to.write(buf, 0, r);
            total += r;
        }
        return total;
    }

    /**
     * Reads all bytes from an input stream into a byte array.
     * Does not close the stream.
     *
     * @param in the input stream to read from
     * @return a byte array containing all the bytes from the stream
     * @throws IOException if an I/O error occurs
     */
    private byte[] toByteArray(InputStream in) throws IOException {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        copy(in, out);
        return out.toByteArray();
    }
}
